#!/usr/bin/perl -w

=head1 SYNOPSIS

psnup2.pl - a better psnup

For details, see http://consodoc.com/psnup2/

=head1 USAGE

psnup2.pl -d -m <margin> -w <page_width> -h <page_height>
          -l <n_long_edge> -s <n_short_edge> -r <reverse> -D
          SOURCE [DEST]

-d  If specified, only prints out the command.
-m  Margin, in centimeters, that should be kept around the page. Default to 1.
-w  Resulting page width, in centimeters. Default to 21.
-h  Resulting page height, in centimeters. Default to 29.7.
-l  Number of pages to fit on the long edge. Default to 2.
-s  Number of pages to fit on the short edge. Default to 1.
-r  If specified, the order is reversed on the long edge.
-D  Rotation direction, if needed ("L" or "R"). Default to "L".

=head1 REQUIRE

Requires psutils installed and available in the command line
http://www.tardis.ed.ac.uk/~ajcd/psutils/

=head1 AUTHOR

Public domain, (c) Oleg Parashchenko, Lionel Guy

=head1 VERSION

Version: 0.0.5, 23 October 2008

=cut

use strict;
use Getopt::Std;

#
# Set default options
#
our ($opt_d, $opt_r);
our $opt_m = 1; # in centimeters
our ($opt_w, $opt_h) = (21.0, 29.7); # in centimeters, A4
our ($opt_l, $opt_s) = (2, 1); # Default: 2 pages per page

our $opt_D = 'L'; # 'L' or 'R'. How to rotate, if required.

my $units_per_cm = 72 * .3937; # 1 centimeters = .393700787 inches, 1 inch = 72 PostScript units5

#
# Parse command line
#
getopts("dm:w:h:l:s:r");

my ($in_file, $out_file) = @ARGV;
die "No input file" unless $in_file;

#
# Calculate the maximal bounding box
#
my ($x1, $y1, $x2, $y2) = (32000, 32000, -32000, -32000);
my $cmdline = "gs -sDEVICE=bbox -dBATCH -dNOPAUSE $in_file 2>&1 |";
(open FIN, $cmdline) or die "Can't run: '$cmdline'";
while (defined (my $l = <FIN>)) {
    #print $l;
    if ($l =~ m/^\%\%BoundingBox: (\d+) (\d+) (\d+) (\d+)/) {
	$x1 = $1 if $1 < $x1;
	$y1 = $2 if $2 < $y1;
	$x2 = $3 if $3 > $x2;
	$y2 = $4 if $4 > $y2;
    }
}
close FIN;
die "Error calculating bounding box" if (($x1 >= $x2) || ($y1 >= $y2));
#print "Bounding box: ($x1,$y1), ($x2,$y2)\n";
my @bbox = ($x1, $y1, $x2, $y2);

#
# Calculate pstops specification
#
my @views = &calculate_views_short_x_long($opt_s, $opt_l, $opt_r);

my $spec = scalar(@views);
for (my $i = 0; $i < scalar(@views); $i++) {
    my $s = &calc_pstops_page(\@bbox, $views[$i]);
    $spec .= ($i ? '+' : ':') . $i. $s;
}
my ($w, $h) = ($opt_w * $units_per_cm, $opt_h * $units_per_cm);
$w = int($w + .5);
$h = int($h + .5);
my $ps_size_spec = "\%\%BeginFeature: *PageSize ($w $h)\n<< /PageSize [$w $h] >> setpagedevice\n\%\%EndFeature\n";

#
# Dry run? Only print the specification
#
$cmdline = "pstops -w$w -h$h '$spec' $in_file";
if ($opt_d) {
    print "$cmdline $out_file\nAnd add after the first '\%\%EndComments':\n$ps_size_spec";
    exit(0);
}

#
# Run the program and filter the output
#
my $PStoPSclip_hack = 1;
$out_file = '-' unless $out_file;
(open FOUT, ">$out_file") or die "Can't create '$out_file': $!\n";
(open FIN, "$cmdline|") or die "Can't run '$cmdline': $!\n";
while (my $l = <FIN>) {
    # Optional, but nice: tune how "gv" will show the document
    next if $l =~ m/^\%\%DocumentMedia:/;
    if ($l =~ m/^\%\%BoundingBox:/) {
	(print FOUT "\%\%BoundingBox: 0 0 $w $h\n") or die "Can't print: $!";
	next;
    }
    (print FOUT $l) or die "Can't print: $!";
    chomp $l;
    # Important to print the document right
    if ('%%EndComments' eq $l) {
	(print FOUT $ps_size_spec) or die "Can't print: $!";
	last;
    }
}
while (my $l = <FIN>) {
    (print FOUT $l) or die "Can't print: $!";
    if ($PStoPSclip_hack && ($l =~ m/^userdict\/PStoPSclip{0 0 moveto$/)) {
	$l = <FIN>;
	$l =~ s/\./0./g; # Increase clipping box by 10
	(print FOUT $l) or die "Can't print: $!";
    }
}
(close FIN) or die "Can't close '$cmdline': $!";
(close FOUT) or die "Can't close '$out_file': $!";

# =========================================================

#
# Calculate an item of the pstops specification
#
sub calc_pstops_page {
    my ($box_from, $box_to) = @_;
    #print 'bbox_from: ', join(' ', @$box_from), "\n";
    #print 'bbox_to:   ', join(' ', @$box_to), "\n";
    #
    # Check if rotation required
    #
    my ($width_from, $height_from) = ($box_from->[2] - $box_from->[0], 
				      $box_from->[3] - $box_from->[1]);
    my ($width_to, $height_to) = ($box_to->[2] - $box_to->[0], 
				  $box_to->[3] - $box_to->[1]);
    my $rotation = (($width_from > $height_from) xor ($width_to > $height_to));
    #
    # Scale factor
    #
    my ($scale1, $scale2);
    if ($rotation) {
	($scale1, $scale2) = ($height_to / $width_from, 
			      $width_to / $height_from);    
    } else {
	($scale1, $scale2) = ($width_to / $width_from, 
			      $height_to / $height_from);
    }
    my $scale = ($scale1 > $scale2) ? $scale2 : $scale1;
    #print "scale 1,2,common: [$scale1] [$scale2] [$scale]\n";
    #
    # Calculate the centers of the boxes
    #
    my ($cx, $cy) = (.5 * ($box_from->[0] + $box_from->[2]), 
		     .5 * ($box_from->[1] + $box_from->[3]));
    my ($cx_to, $cy_to) = (.5 * ($box_to->[0] + $box_to->[2]), 
			   .5 * ($box_to->[1] + $box_to->[3]));
    #
    # Fist, pstops scales, then rotates, then moves
    #
    ($cx, $cy) = ($cx * $scale, $cy * $scale);
    if ($rotation) {
	$rotation = $opt_D;
	if ('L' eq $rotation) {
	    ($cx, $cy) = (-$cy, $cx);
	} elsif ('R' eq $rotation) {
	    ($cx, $cy) = ($cy, -$cx);
	} else {
	    die "Unknown rotation";
	}
    }
    my ($movex, $movey) = ($cx_to - $cx, $cy_to - $cy);
    #
    # Generate the summary
    #
    my $s = sprintf('%s@%.3f(%.1f,%.1f)', $rotation, $scale, $movex, $movey);
    #print "pstops string: [$s]\n";
    return $s;
}

# =========================================================

# 
# Calculate coordinates of splitting the dimension on K chunks
# Returns an array, each item is a reference to an array of two
#   elements: the begin and end coordinates
#
sub calculate_coordinates {
    my ($length, $opt_m, $n) = @_;
    my $skip  = ($length - $opt_m) / $n;
    my $width = $skip - $opt_m;
    my @end_coords = map { int(.5 + $skip * $_) } (1..$n);
    my @coords = map { my @a = (int(.5 + $_ - $width), $_); \@a } @end_coords;
    #print "$n: "; map { print '[', $_->[0], ',', $_->[1], ']' } @coords; print "\n";
    return @coords;
}

#
# N_short_edge * N_long_edge views paer page
# (For A4, short == width, long == height)
#
sub calculate_views_short_x_long {
    my ($n_short_edge, $n_long_edge, $long_reverse) = @_;
    my ($pu_width, $pu_height, $pu_margin) 
	= ($opt_w * $units_per_cm, $opt_h * $units_per_cm, 
	   $opt_m * $units_per_cm);
    my @short_coordinates = 
	&calculate_coordinates($pu_width,  $pu_margin, $n_short_edge);
    my @long_coordinates  = 
	&calculate_coordinates($pu_height, $pu_margin, $n_long_edge);
    if ($long_reverse) {
	@long_coordinates = reverse @long_coordinates;
    }
    my @views;
    foreach my $x_coords (@short_coordinates) {
	foreach my $y_coords (@long_coordinates) {
	    my @view = ($x_coords->[0], $y_coords->[0], 
			$x_coords->[1], $y_coords->[1]);
	    push @views, \@view;
	}
    }
    return @views;
}



